/***************************************************************
 *
 * OpenBeacon.org - virtual FAT16 file system support
 *
 * Copyright 2010 Milosch Meriac <meriac@openbeacon.de>
 *
 ***************************************************************

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; version 2.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

*/
#include <openbeacon.h>
#include "msd.h"
#include "vfs.h"

#ifdef USB_DISK_SUPPORT

#define VOLUME_START 1UL
#define VOLUME_SECTORS (DISK_SECTORS-VOLUME_START)
#define DISK_SECTORS_PER_CLUSTER 8UL
#define DISK_CLUSTER_SIZE (DISK_SECTORS_PER_CLUSTER*DISK_BLOCK_SIZE)
#define FAT16_SIZE_SECTORS ((uint32_t)((((VOLUME_SECTORS/DISK_SECTORS_PER_CLUSTER+2)*2)+DISK_BLOCK_SIZE-1)/DISK_BLOCK_SIZE))
#define RESERVED_SECTORS_COUNT 1UL
#define BPB_NUMFATS 2UL
#define ROOT_DIR_SECTORS 1UL
#define MAX_FILES_IN_ROOT ((ROOT_DIR_SECTORS*DISK_BLOCK_SIZE)/sizeof(TDiskDirectoryEntry))
#define FIRST_FAT_SECTOR (RESERVED_SECTORS_COUNT)
#define FIRST_ROOT_DIR_SECTOR (FIRST_FAT_SECTOR+(BPB_NUMFATS*FAT16_SIZE_SECTORS))
#define FIRST_DATA_SECTOR (FIRST_ROOT_DIR_SECTOR+ROOT_DIR_SECTORS)
#define DATA_SECTORS (DISK_SECTORS-FIRST_DATA_SECTOR)
#define FIRST_SECTOR_OF_CLUSTER(N) (((N-2UL)*DISK_SECTORS_PER_CLUSTER)+FIRST_DATA_SECTOR)
#define BPB_MEDIA_TYPE 0xF8
#define DISK_FAT_EOC (0xFF00|BPB_MEDIA_TYPE)

#define DATA_BYTE_START ((VOLUME_START + FIRST_DATA_SECTOR) * DISK_BLOCK_SIZE)
#define DATA_BYTE_END   (DATA_BYTE_START + (DATA_SECTORS * DISK_BLOCK_SIZE))

static const TDiskFile *root_directory = NULL;

typedef uint16_t TFatCluster;

typedef struct TDiskRecord
{
	uint32_t offset, length;
	TDiskHandler handler;
	const void *data;
} TDiskRecord;

typedef struct
{
	uint8_t head, sector, cylinder;
} PACKED TDiskPartitionTableEntryCHS;

typedef struct
{
	uint8_t bootable;
	TDiskPartitionTableEntryCHS start;
	uint8_t partition_type;
	TDiskPartitionTableEntryCHS end;
	uint32_t start_lba;
	uint32_t length;
} PACKED TDiskPartitionTableEntry;

#define ATTR_READ_ONLY  0x01
#define ATTR_HIDDEN     0x02
#define ATTR_SYSTEM     0x04
#define ATTR_VOLUME_ID  0x08
#define ATTR_DIRECTORY  0x10
#define ATTR_ARCHIVE    0x20
#define ATTR_LONG_NAME  (ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | ATTR_VOLUME_ID)

typedef struct
{
	char DIR_Name[11];
	uint8_t DIR_Attr;
	uint8_t DIR_NTRes;
	uint8_t DIR_CrtTimeTenth;
	uint16_t DIR_CrtTime;
	uint16_t DIR_CrtDate;
	uint16_t DIR_LstAccDate;
	uint16_t DIR_FstClusHI;
	uint16_t DIR_WrtTime;
	uint16_t DIR_WrtDate;
	uint16_t DIR_FstClusLO;
	uint32_t DIR_FileSize;
} PACKED TDiskDirectoryEntry;

/* disk signature */
const uint32_t DiskSignature = 0x1B07CF6E;

/* BPB - BIOS Parameter Block: actual volume boot block */
const TDiskBPB DiskBPB = {
	.BS_jmpBoot = {0xEB, 0x00, 0x90},
	.BS_OEMName = "MSWIN4.1",
	.BPB_BytsPerSec = DISK_BLOCK_SIZE,
	.BPB_SecPerClus = DISK_SECTORS_PER_CLUSTER,
	.BPB_RsvdSecCnt = RESERVED_SECTORS_COUNT,
	.BPB_NumFATs = BPB_NUMFATS,
	.BPB_RootEntCnt = MAX_FILES_IN_ROOT,
	.BPB_Media = BPB_MEDIA_TYPE,
	.BPB_FATSz16 = FAT16_SIZE_SECTORS,
	.BPB_SecPerTrk = DISK_SECTORS_PER_TRACK,
	.BPB_NumHeads = DISK_HEADS,
	.BPB_HiddSec = VOLUME_START,
	.BPB_TotSec32 = VOLUME_SECTORS,
	/* FAT12/FAT16 definition */
	.BS_DrvNum = 0x80,
	.BS_BootSig = 0x29,
	.BS_VolID = 0xe9d9489f,
	.BS_VolLab = "OPENBEACON ",
	.BS_FilSysType = "FAT16   ",
};

static void
msd_access_data_area (uint32_t offset, uint32_t length, const void *write,
					  void *read)
{
	uint32_t t;
	uint32_t cluster, cluster_start, cluster_end, cluster_count;
	uint32_t cluster_file_count;

	/* remember variable content over calls to enable caching */
	static const TDiskFile *file = NULL;
	static uint32_t cluster_file_start, cluster_file_end;

	/* ignore zero size read requests */
	if (!length)
		return;

	/* get extent of read/write request */
	cluster_start = offset / DISK_CLUSTER_SIZE;
	cluster_count = (length + DISK_CLUSTER_SIZE - 1) / DISK_CLUSTER_SIZE;
	cluster_end = cluster_start + cluster_count - 1;

	/* check for cached file request */
	if (file && (cluster_end >= cluster_file_start)
		&& (cluster_start <= cluster_file_end))
	{
		if (file->handler)
			file->handler (offset -
						   (cluster_file_start * DISK_CLUSTER_SIZE),
						   length, write, read);
		else
			if (read)
				memset (read, 0, length);
		return;
	}

	/* if no file cached, try to find file */
	cluster = 0;
	file = root_directory;
	while (file)
	{
		if (file->length)
		{
			cluster_file_start = cluster;
			cluster_file_count =
				(file->length + DISK_CLUSTER_SIZE - 1) / DISK_CLUSTER_SIZE;
			cluster_file_end = cluster_file_start + cluster_file_count - 1;

			if ((cluster_end >= cluster_file_start)
				&& (cluster_start <= cluster_file_end))
			{
				/* normalize offset to file start */
				offset -= (cluster_file_start * DISK_CLUSTER_SIZE);

				/* if handler exists - run */
				if (file->handler)
					file->handler (offset, length, write, read);
				/* if no handler exists copy data */
				else if ((file->data) && (offset < file->length))
				{
					/* calculate remaining length */
					if ((t = file->length - offset) > length)
						t = length;

					/* copy data to output buffer */
					if (read)
					{
						memcpy (read, ((uint8_t *) file->data) + offset, t);

						/* if data is smaller, sent remainder to zero */
						if (t < length)
							memset (((uint8_t *) read) + t, 0, length - t);
					}
					else if (write)
						memcpy (((uint8_t *) file->data) + offset, write, t);
				}
				/* if no data exists, set to zero */
				else if (read)
					memset (read, 0, length);

				/* request could be satisfied - bail out with cached file
				   handle after handling request */
				return;
			}

			cluster += cluster_file_count;
		}
		file = file->next;
	}

	/* didn't find a matching request - reset cached file to NULL */
	file = NULL;
}

static void
msd_read_fat_area (uint32_t offset, uint32_t length, const void *write,
				   void *read)
{
	const TDiskFile *file;
	uint32_t count, t, index, remaining;
	uint32_t cluster, cluster_start, cluster_end, cluster_count;
	uint32_t cluster_file_start, cluster_file_end, cluster_file_count;
	TFatCluster *fat;

	/* ignore writes */
	if (write)
		return;

	/* ignore read requests for sizes < cluster table granularity */
	if (length < sizeof (TFatCluster))
		return;

	/* get extent of read/write request */
	cluster_start = offset / sizeof (TFatCluster);
	cluster_count = length / sizeof (TFatCluster);
	cluster_end = cluster_start + cluster_count - 1;

	/* pre-set FAT */
	fat = (TFatCluster *) read;
	t = cluster_count;

	/* special treatment for reserved clusters */
	if (!offset)
	{
		/* indicate media type */
		*fat++ = DISK_FAT_EOC;
		/* indicate clean volume */
		*fat++ = (0x8000 | 0x4000);
		t -= 2;
	}

	/* mark all custers as bad by default to make
	   sure there is no free space left on the disk
	   for new files */
	while (t--)
		*fat++ = 0xFFF7;

	/* first cluster number on disk is 2 */
	cluster = 2;
	file = root_directory;
	while (file)
	{
		if (file->length)
		{
			cluster_file_start = cluster;
			cluster_file_count =
				(file->length + DISK_CLUSTER_SIZE - 1) / DISK_CLUSTER_SIZE;
			cluster_file_end = cluster_file_start + cluster_file_count - 1;

			if ((cluster_end >= cluster_file_start)
				&& (cluster_start < cluster_file_end))
			{
				if (cluster_file_start < cluster_start)
				{
					index = cluster_start - cluster_file_start;
					remaining = cluster_file_count - index;
					index += cluster;
					count =
						(cluster_count >
						 remaining) ? remaining : cluster_count;
					fat = (TFatCluster *) read;
				}
				else
				{
					index = cluster;
					remaining = cluster_file_count;
					t = (cluster_file_start - cluster_start);
					count = cluster_count - t;
					fat = ((TFatCluster *) read) + t;
				}

				/* populate FAT entries with linked FAT list */
				if (remaining)
					for (t = 1; t <= count; t++)
						if (remaining-- > 1)
							*fat++ = index + t;
						else
						{
							/* set last entry to EOC (End Of Chain) */
							*fat = DISK_FAT_EOC;
							break;
						}
			}
			cluster += cluster_file_count;
		}
		file = file->next;
	}
}

static void
msd_read_root_dir (uint32_t offset, uint32_t length, const void *write,
				   void *read)
{
	uint32_t cluster, t;
	const TDiskFile *file;
	TDiskDirectoryEntry *entry;

	/* ignore writes */
	if (write)
		return;

	/* initialize response with zero */
	memset (read, 0, length);

	/* first cluster number on disk is 2 */
	cluster = 2;
	/* skip first entries */
	file = root_directory;
	t = offset / sizeof (TDiskDirectoryEntry);
	while (t--)
	{
		if (!file)
			return;
		cluster += (file->length + DISK_CLUSTER_SIZE - 1) / DISK_CLUSTER_SIZE;
		file = file->next;
	}

	entry = (TDiskDirectoryEntry *) read;
	t = length / sizeof (TDiskDirectoryEntry);
	while (t--)
	{
		if (!file)
			return;

		/* turn last entry to volume ID */
		entry->DIR_Attr = file->next ? ATTR_READ_ONLY : ATTR_VOLUME_ID;

		/* populate directory entry */
		entry->DIR_FileSize = file->length;
		entry->DIR_FstClusLO = (file->length) ? cluster : 0;
		strncpy (entry->DIR_Name, file->name, sizeof (entry->DIR_Name));

		/* increment cluster pos */
		cluster += (file->length + DISK_CLUSTER_SIZE - 1) / DISK_CLUSTER_SIZE;

		/* go to next entry */
		entry++;
		file = file->next;
	}
}

static void
msd_read_memory (uint32_t offset, uint32_t length, const void *write,
				 void *read)
{
	memcpy (read, ((const uint8_t *) write) + offset, length);
}

void
msd_read (uint32_t offset, uint8_t * read, uint32_t length)
{
	const TDiskRecord *rec;
	uint32_t t, read_end, rec_start, rec_end, pos, count, written;
	uint8_t *p;


	/* first partition table entry */
	static const TDiskPartitionTableEntry DiskPartitionTableEntry = {
		.bootable = 0x00,
		.start = {
				  .head = 0,
				  .sector = VOLUME_START + 1,
				  .cylinder = 0},
		.partition_type = 0x06,
		.end = {
				.head = DISK_HEADS - 1,
				.sector = DISK_SECTORS_PER_TRACK,
				.cylinder = DISK_CYLINDERS - 1},
		.start_lba = 1,
		.length = VOLUME_SECTORS
	};

	/* MBR termination signature */
	static const uint16_t BootSignature = 0xAA55;


	/* data mapping of virtual drive */
	static const TDiskRecord DiskRecord[] = {
		/* data area */
		{
		 .offset = (VOLUME_START + FIRST_DATA_SECTOR) * DISK_BLOCK_SIZE,
		 .length = DATA_SECTORS * DISK_BLOCK_SIZE,
		 .handler = msd_access_data_area,
		 .data = NULL}
		,
		/* FAT area - primary copy */
		{
		 .offset = (VOLUME_START + FIRST_FAT_SECTOR) * DISK_BLOCK_SIZE,
		 .length = FAT16_SIZE_SECTORS * DISK_BLOCK_SIZE,
		 .handler = msd_read_fat_area,
		 .data = NULL}
		,
		/* FAT area - secondary copy */
		{
		 .offset =
		 (VOLUME_START + FIRST_FAT_SECTOR +
		  FAT16_SIZE_SECTORS) * DISK_BLOCK_SIZE,
		 .length = FAT16_SIZE_SECTORS * DISK_BLOCK_SIZE,
		 .handler = msd_read_fat_area,
		 .data = NULL}
		,
		/* root dir  */
		{
		 .offset = (VOLUME_START + FIRST_ROOT_DIR_SECTOR) * DISK_BLOCK_SIZE,
		 .length = ROOT_DIR_SECTORS * DISK_BLOCK_SIZE,
		 .handler = msd_read_root_dir,
		 .data = NULL}
		,
		/* disk signature */
		{
		 .offset = 0x1B8,
		 .length = sizeof (DiskSignature),
		 .handler = msd_read_memory,
		 .data = &DiskSignature}
		,
		/* first partition table entry */
		{
		 .offset = 0x1BE,
		 .length = sizeof (DiskPartitionTableEntry),
		 .handler = msd_read_memory,
		 .data = &DiskPartitionTableEntry}
		,
		/* MBR termination signature */
		{
		 .offset = 0x1FE,
		 .length = sizeof (BootSignature),
		 .handler = msd_read_memory,
		 .data = &BootSignature}
		,
		/* BPB - BIOS Parameter Block: actual volume boot block */
		{
		 .offset = (VOLUME_START * DISK_BLOCK_SIZE),
		 .length = sizeof (DiskBPB),
		 .handler = msd_read_memory,
		 .data = &DiskBPB}
		,
		/* BPB termination signature */
		{
		 .offset = (VOLUME_START * DISK_BLOCK_SIZE) + 0x1FE,
		 .length = sizeof (BootSignature),
		 .handler = msd_read_memory,
		 .data = &BootSignature}
		,
	};

	/* ignore zero reads and reads outside of disk area */
	if (!length || (offset >= DISK_SIZE))
		return;

	/* iterate DiskRecords and fill request with content */
	rec = DiskRecord;
	t = sizeof (DiskRecord) / sizeof (DiskRecord[0]);
	read_end = offset + length;
	written = 0;

	while (t--)
	{
		rec_start = rec->offset;
		rec_end = rec_start + rec->length;

		if ((read_end >= rec_start) && (offset < rec_end))
		{
			if (rec_start >= offset)
			{
				pos = 0;
				p = &(((uint8_t *) read)[rec_start - offset]);
				count = (rec_end <= read_end) ?
					rec->length : read_end - rec_start;
			}
			else
			{
				pos = offset - rec_start;
				p = read;
				count = (read_end > rec_end) ? rec_end - offset : length;
			}

			/* set memory of partial read to zero before filling up */
			if (!written && (count != length))
				memset (read, 0, length);

			/* handle request */
			rec->handler (pos, count, rec->data, p);
			written += count;

			/* all bytes handled -> quit */
			if (written == length)
				break;
		}
		rec++;
	}

	/* set memory to zero if not written yet  */
	if (!written)
		memset (read, 0, length);
}

void
msd_write (uint32_t offset, uint8_t * write, uint32_t length)
{
	if (!write || !length)
		return;

	/* ignore writes outside of data area */
	if ((offset < DATA_BYTE_START) || ((offset + length) > DATA_BYTE_END))
		return;

	/* remove absolute offset into data area */
	offset -= DATA_BYTE_START;

	msd_access_data_area (offset, length, write, NULL);
}

void
vfs_status (void)
{
}

void
vfs_init (const TDiskFile * file, uint8_t connect)
{
	if (file)
	{
		root_directory = file;
		/* init USB mass storage */
		msd_init (connect);
	}
}

#endif /* USB_DISK_SUPPORT */
